#!/usr/bin/env bash

#
# Check if we need to copy node_modules.
# (e.g. in case it has been modified after last copy)
#
# The reasoning for a (mostly) read-only node_modules folder:
#   ideally we’d symlink the folder directly to the Nix store
#   so that we’re guaranteed to have a reproducible source.
#   Unfortunately react-native wants to build some stuff after the fact
#   and this is incompatible with the concept of a pure Nix package.
#   Therefore we copy the whole source to the repo directory,
#   allow writing only on the folders where it is absolutely required,
#   and therefore we still keep some peace of mind that the rest
#   of node_modules is unchanged the rest of the time.
#

set -Eeuo pipefail

GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel)
source "${GIT_ROOT}/scripts/colors.sh"

# More concise output from 'time'
export TIMEFORMAT="Done in: %Es"

removeDir() {
    [[ ! -d "${tmp}" ]] && return
    chmod -R u+w "${tmp}"
    rm -rf "${tmp}"
}

copyNodeModules() {
  local src="${1}"
  local dst="${2}"
  # WARNING: The ../ is there to avoid a Nix builtins.path bug:
  # https://github.com/NixOS/nix/issues/3593
  local tmp=$(mktemp -d -p "$(dirname "${dst}")/../")

  # We use a temporary directory to use mv as "atomic" change
  trap "removeDir ${tmp}" ERR INT HUP

  # WARNING: The -L here is crucial to let Metro find modules.
  cp -LRf ${src}/node_modules/. "${tmp}"
  chmod -R +w "${tmp}"

  # WARNING: We can't de-reference .bin symlinks
  cp -Rf ${src}/node_modules/.bin/. "${tmp}/.bin/"
  # Cmake called by react-native-reanimated 3.3.0 during build creates read-only temporary files.
  chmod -R u+w node_modules/react-native-reanimated/android/ -f || true
  rm -r "${dst}"
  mv -f "${tmp}" "${dst}"
}

# Find files that were modified and should cause a re-copying of node modules.
# Some files are generated/modified by build processes and should be ignored.
findFilesNewerThan() {
  local sentinel="${1}"
  local dir="${2}"
  find "${dir}" -type f -writable \
      -newer "${sentinel}" \
      -not -ipath "*/*android/build/*" -prune \
      -not -ipath "*/xcuserdata/*" -prune \
      -not -ipath "*/scripts/.packager.env" \
      \ # generated at runtime by react-native \
      \ # related code https://github.com/facebook/react-native/blob/v0.72.5/packages/react-native/ReactCommon/React-rncore.podspec#L19 \
      -not -path "*/node_modules/react-native/ReactCommon/react/renderer/components/rncore/*" \
      \ # generated at runtime by react-native-config \
      \ # related code https://github.com/lugg/react-native-config/blob/v1.5.0/react-native-config.podspec#L52 \
      -not -path "*/node_modules/react-native-config/ios/ReactNativeConfig/GeneratedDotEnv.m" \
      \ # generated at runtime by react-native from v0.73.x onwards \
      -not -path "*/node_modules/react-native/React/Fabric/RCTThirdPartyFabricComponentsProvider.mm" \
      -print
}

nodeModulesUnchanged() {
  local src="$1"
  local dst="$2"
  local sentinelFile="${dst}/.copied~"

  # Check if node_modules exists and is valid
  if [[ ! -f "${sentinelFile}" ]]; then
      # node_modules have not been created by this script
      echo -e "${YLW}Node modules not created by Nix${RST}" >&2
      return 1
  fi

  # Sentinel file holds location of the node_modules source in Nix store
  currentNixSrc="$(cat "${sentinelFile}")"

  if [ "${currentNixSrc}" != "${src}" ]; then
    echo -e "${YLW}Yarn modules changed, copying new version over${RST}" >&2
    return 1
  fi

  # Some build processes modify files in node_modules
  modifiedFiles=($(findFilesNewerThan "${sentinelFile}" "${dst}"))
  if [ ${#modifiedFiles[@]} -ne 0 ]; then
    echo -e "${YLW}Changes detected in node_modules:${RST} ${#modifiedFiles[@]}" >&2
    # Print files that have changes
    for file in ${modifiedFiles[@]}; do
      echo "- $(realpath --relative-to="${dst}" "${file}")" >&2
    done
    return 1
  fi

  echo -e "${GRN}No changes detected.${RST}" >&2
  return 0
}

replaceNodeModules() {
  local src="$1"
  local dst="$2"
  local sentinelFile="${dst}/.copied~"

  if nodeModulesUnchanged "${src}" "${dst}"; then
      return
  fi

  # Replace node_modules if necessary
  echo "Copying node_modules from Nix store:" >&2
  echo " - ${src}" >&2
  copyNodeModules "${src}" "${dst}"
  echo -n "${src}" > "${sentinelFile}"
}

# Destination folder, Nix sets STATUS_MOBILE_HOME
dst="$STATUS_MOBILE_HOME/node_modules"
# Source of Node modules from /nix/store
src="$1"

if [[ ! -d ${src} ]]; then
    echo -e "${RED}No such folder:${RST} ${src}" >&2
    exit 1
fi

# Make those available in shell spawned by flock
export -f replaceNodeModules nodeModulesUnchanged copyNodeModules findFilesNewerThan removeDir

mkdir -p "${dst}"
# Leverage file lock to create an exclusive lock.
# Otherwise multiple calls to this script would clash.
flock "${dst}/" sh -c "time replaceNodeModules '${src}' '${dst}'"
